#pragma
#include<iostream>
#include<string>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<strings.h>
#include<functional>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
using namespace std;

static const string defaultIp = "0.0.0.0"; //这里给0就是为了起到和INADDR_ANY一样的效果

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    OPEN_ERR
};

typedef function<void (int, string, uint16_t, string)> func_t;

class udpServer
{
public:
    udpServer(const func_t& cb, const uint16_t& port, const string ip = defaultIp)
    :_port(port)
    ,_ip(ip)
    ,_sockfd(-1)
    ,_callback(cb)
    {}

    void initServer()
    {
        //1.创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); //创建套接字【AF_INET表示网络通信， SOCK_DGRAM表示udp】
        if(_sockfd == -1)
        {
            // 文件描述符为-1就是出错了
            cerr << "socket error: " << errno << ": " << strerror(errno) << endl;
            exit(SOCKET_ERR);
        }
        cout << "socket success: " << _sockfd << endl;
        // 创建好套接字之后，就要给服务器绑定端口号和ip地址
        // 服务器要显式的bind，必须指明端口号，不能随意改变
        
        // 2.绑定port和ip
        // 2.1【先填充结构体】
        struct sockaddr_in local; // sockaddr_in这个结构体要#include<arpa/inet.h>
        bzero(&local, sizeof(local)); // 对一段指定大小的地址填一堆0进去
        local.sin_family = AF_INET; //指定结构体内部的协议，即未来应用于什么场景(网络通信)
        local.sin_port = htons(_port); //htons即做网络字节序的转化【大小端转化, 统一转为大端】
        // 实际上服务器并不会显式的单一的bind一个ip地址，真实的做法是任意地址bind
        // 这样只要给当前机器的port发送的消息，发给本机器任何ip的都可以接受到
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); //inet_addr对_ip进行类型转化, _ip是string类型的，最终要转为32位的整数
        // local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意地址bind，不会漏掉发给属于该机器的其他ip的数据

        // 此时结构体设置好了通信协议，port，和ip，但是os根本不知道，这就是结构体对象，还没有传给os
        
        // 2.2填充结构体之后就要和os关联、【其实就是和指定的网络的ip和端口产生关联】
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local)); //要注意强转
        if(n == -1)
        {
            // n为-1就是关联失败了, 虽然成功和网卡文件取得联系，但是服务器端还没有办法和网络取得关联
            cerr << "bind error: " << errno << ": " << strerror(errno) << endl;
            exit(BIND_ERR);
        }
        // 此时udp服务器预备工作完成
    }

    // 类似于服务器这种常驻内存进程，一旦开启，一般不轻易结束。【因此也最怕内存泄漏的情况出现】
    void start()
    {
        // 服务器的本质就是一个死循环[不断的检查是否有客户端数据传输]
        char buffer[4096];
        for(;;)
        {
            // 服务器要能够在网络中读写数据
            
            // 读取数据调用recvfrom
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            // 走到这里就是读取成功，此时可以通过结构体查看客户端信息
            if(s > 0)
            {
                string clientip = inet_ntoa(peer.sin_addr); //inet_ntoa将int类型ip地址，转化为点分十进制，并将网络序列转化为主机序列
                uint16_t clientport = ntohs(peer.sin_port); //ntohs将网络字节序转化为主机序列
                buffer[s] = 0;
                string message = buffer;

                cout << clientip << "[" << clientport << "] send: " << message << endl;
                // 处理读取到的数据, 并将处理完的数据发送给客户端
                _callback(_sockfd, clientip, clientport, message);
            }
        }
    }

    ~udpServer()
    {}
private:
    uint16_t _port; //端口号
    string _ip; //ip地址【实际上，这里并不会只显式绑定一个ip地址，这样会漏掉发送给属于这个服务器其他ip的数据，真实的做法是任意地址bind】
    int _sockfd; //文件描述符
    func_t _callback; //回调函数
};